﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CommonLib {
    public class Tracer {
        private Screen screen;
        private Scene scene;
        private Random random;

        public Tracer(Screen scr, Scene sce) {
            this.screen = scr;
            this.scene = sce;
            this.random = new Random();
        }

        public double Random { get { lock (this) { return random.NextDouble(); } } }
        public Screen Screen {
            get { return this.screen; }
        }
        public Primitive findNearest(Ray ray, ref double distance, ref Orientation orientation) {
            Primitive nearest = null;

            for (int j = 0; j < scene.Primitives.Length; j++) {
                Primitive tmp = scene.Primitives[j];
                Orientation currOri;

                currOri = tmp.isIntersect(ray, ref distance);
                if (currOri == Orientation.Nonexistent)
                    continue;
                nearest = tmp;
                orientation = currOri;
            }
            return nearest;
        }
        public Primitive findNearestFast(Ray ray, ref double distance, ref Orientation orientation) {
            //Vector3c orig = ray.position;
            //orig.sub(scene.Grid.boundary.vmin);
            //orig.mul(scene.Grid.rdiff);
            //Int3 idx = new Int3(orig);
            var Grid = scene.Grid;
            var Size = Grid.size;
            var VMin = Grid.boundary.vmin;
            var Rdiff = Grid.rdiff;
            var Diff = Grid.diff;
            Int3 idx = new Int3(
                (int)((ray.position.x - VMin.x) * Rdiff.x),
                (int)((ray.position.y - VMin.y) * Rdiff.y),
                (int)((ray.position.z - VMin.z) * Rdiff.z));

            if (idx.x < 0 || Size <= idx.x
                || idx.y < 0 || Size <= idx.y
                || idx.z < 0 || Size <= idx.z)
                return null;
            
            Int3 dsign = new Int3();
            Int3 sentry = new Int3();
            Vector3c edge = VMin;
            if (ray.direction.x > 0) {
                dsign.x = 1;
                sentry.x = Size;
                edge.x += (idx.x + 1) * Diff.x;
            } else {
                dsign.x = -1;
                sentry.x = -1;
                edge.x += (idx.x) * Diff.x;
            }
            if (ray.direction.y > 0) {
                dsign.y = 1;
                sentry.y = Size;
                edge.y += (idx.y + 1) * Diff.y;
            } else {
                dsign.y = -1;
                sentry.y = -1;
                edge.y += (idx.y) * Diff.y;
            }
            if (ray.direction.z > 0) {
                dsign.z = 1;
                sentry.z = Size;
                edge.z += (idx.z + 1) * Diff.z;
            } else {
                dsign.z = -1;
                sentry.z = -1;
                edge.z += (idx.z) * Diff.z;
            }

            Vector3c tmax = new Vector3c(1.0e6, 1.0e6, 1.0e6);
            Vector3c tdelta = new Vector3c();
            if (ray.direction.x != 0) {
                double rdir = 1.0 / ray.direction.x;
                tmax.x = (edge.x - ray.position.x) * rdir;
                tdelta.x = dsign.x * (Diff.x * rdir);
            }
            if (ray.direction.y != 0) {
                double rdir = 1.0 / ray.direction.y;
                tmax.y = (edge.y - ray.position.y) * rdir;
                tdelta.y = dsign.y * (Diff.y * rdir);
            }
            if (ray.direction.z != 0) {
                double rdir = 1.0 / ray.direction.z;
                tmax.z = (edge.z - ray.position.z) * rdir;
                tdelta.z = dsign.z * (Diff.z * rdir);
            }

            Primitive nearestPrim = null;
            Orientation currOri;
            Primitive primCache = null;
            while (true) {
                List<Primitive> list = Grid.grid[idx.x + (idx.y + idx.z * Size) * Size];
                foreach (Primitive prim in list) {
                    if (primCache == prim)
                        continue;
                    primCache = prim;

                    currOri = prim.isIntersect(ray, ref distance);
                    if (currOri == Orientation.Nonexistent)
                        continue;
                    nearestPrim = prim;
                    orientation = currOri;
                }
                if (tmax.x < tmax.y) {
                    if (tmax.x < tmax.z) { // x
                        if (distance <= tmax.x)
                            goto TravEnd;
                        idx.x += dsign.x;
                        if (idx.x == sentry.x)
                            return null;
                        tmax.x += tdelta.x;
                    } else { // z
                        if (distance <= tmax.z)
                            goto TravEnd;
                        idx.z += dsign.z;
                        if (idx.z == sentry.z)
                            return null;
                        tmax.z += tdelta.z;
                    }
                } else {
                    if (tmax.y < tmax.z) { // y
                        if (distance <= tmax.y)
                            goto TravEnd;
                        idx.y += dsign.y;
                        if (idx.y == sentry.y)
                            return null;
                        tmax.y += tdelta.y;
                    } else { // z
                        if (distance <= tmax.z)
                            goto TravEnd;
                        idx.z += dsign.z;
                        if (idx.z == sentry.z)
                            return null;
                        tmax.z += tdelta.z;
                    }
                }
            }
        TravEnd:
            return nearestPrim;
        }
        public double estimateShadow(Primitive light, Vector3c node, ref Vector3c dir2light, 
            int incStep) {
            double shadow = 0.0;

            dir2light = light.getCenter();
            dir2light.sub(node);
            double dist2light = dir2light.norm();
            dir2light.normalize();

            Vector3c position = node;
            position.mac(DoubleTester.Epsilon, dir2light);

            if (light is Sphere) {
                Ray ray2light = new Ray(position, dir2light);                
                Orientation eclipsOri = Orientation.Nonexistent;

                if (light.isIntersect(ray2light, ref dist2light) == Orientation.Exterior) {
                    dist2light -= DoubleTester.Epsilon;
                    //Primitive closer = findNearest(ray2light, ref dist2light, ref eclipsOri);
                    Primitive closer = findNearestFast(ray2light, ref dist2light, ref eclipsOri);
                    if (closer == null)
                        shadow = 1.0;
                }
            } else if (light is AABB) {
                AABB box = (AABB) light;
                var Grid = box.lightGrid;
                var Tile = box.lightTile;

                int limit = Grid.Length;
                double dshadow = 1.0 / limit * incStep;

                for (int i = 0; i < limit; i+= incStep) {
                    Vector3c dir2tile = Grid[i];

                    dir2tile.x += Tile.x * Random;
                    dir2tile.z += Tile.z * Random;
                    dir2tile.sub(node);
                    double dist2tile = dir2tile.norm();

                    Ray ray2tile = new Ray(position, dir2tile, dist2tile);
                    Orientation eclipsOri = Orientation.Nonexistent;
                    if (light.isIntersect(ray2tile, ref dist2tile) == Orientation.Exterior) {
                        dist2tile -= DoubleTester.Epsilon;
                        //Primitive closer = findNearest(ray2light, ref dist2light, ref eclipsOri);
                        Primitive closer = findNearestFast(ray2tile, ref dist2tile, ref eclipsOri);
                        if (closer == null)
                            shadow += dshadow;
                    }

                }
            }

            return shadow;

        }

        public Primitive raytrace(Ray ray, ref Vector3c color, ref Vector3c node,
            int depth, int incStep) {
            Primitive nearest = null;
            double distance = Double.MaxValue;
            Orientation orientation = Orientation.Nonexistent;
            Vector3c tmpColor = new Vector3c();
            Vector3c tmpNode = new Vector3c();
            Vector3c tmpDirection = new Vector3c();
            Vector3c tmpPosition = new Vector3c();
            Vector3c color0 = color;

            if (depth <= 0)
                return null;

            //nearest = findNearest(ray, ref distance, ref orientation);
            nearest = findNearestFast(ray, ref distance, ref orientation);
            if (nearest == null) {
                color.set(0, 0, 0);
                return nearest;
            } else if (nearest.light) {
                color.set(1.0, 1.0, 1.0);
                return nearest;
            } else
                color0.set(0, 0, 0);
            node = ray.position;
            node.mac(distance, ray.direction);

            Vector3c primNormal = nearest.getNormal(node);
            primNormal.mul((double) orientation);
            double cosA = -ray.direction.dot(primNormal);
            tmpDirection = ray.direction;
            tmpDirection.mac(2 * cosA, primNormal);

            // diffuse, specular & shadow
            for (int j = 0; j < scene.Lights.Length; j++) {
                Primitive light = scene.Lights[j];
                Vector3c dir2light = new Vector3c();

                //shadow
                double shadow = estimateShadow(light, node, ref dir2light, incStep);

                // diffuse
                if (nearest.material.diffuse > 0) {
                    double arg = primNormal.dot(dir2light);
                    if (0 < arg)
                        color0.mac(nearest.material.diffuse * arg * shadow, light.material.color);
                }

                // specular
                if (nearest.material.specular > 0) {
                    double arg = dir2light.dot(tmpDirection);
                    if (0 < arg)
                        color0.mac(nearest.material.specular * Math.Pow(arg, 20.0) * shadow, light.material.color);
                }
            }
            //color0.add(tmpColor);
            
            // reflection
            if (nearest.material.reflection > 0) {
                tmpPosition = node;
                tmpPosition.mac(DoubleTester.Epsilon, tmpDirection);
                Ray reflRay = new Ray(tmpPosition, tmpDirection);
                raytrace(reflRay, ref tmpColor, ref tmpNode, depth - 1, incStep * 4);
                color0.mac(nearest.material.reflection, tmpColor);
            }
            color0.mul(nearest.getColor(node));

            // refraction
            if (nearest.material.refraction > 0) {
                double nab = (orientation == Orientation.Exterior ?
                    nearest.material.refrIndex : 1.0 / nearest.material.refrIndex);

                double cosB0 = Math.Sqrt(cosA * cosA + nab * nab - 1);
                tmpDirection = ray.direction;
                tmpDirection.mac(-(cosB0 + cosA), primNormal);
                tmpDirection.div(nab);
                tmpPosition = node;
                tmpPosition.mac(DoubleTester.Epsilon, tmpDirection);
                Ray refrRay = new Ray(tmpPosition, tmpDirection);

                if (cosA > 0 && cosB0 > 0) {
                    raytrace(refrRay, ref tmpColor, ref tmpNode, depth - 1, incStep * 4);
                    if (orientation == Orientation.Exterior) {
                        Vector3c transparency = nearest.getColor(node);
                        transparency.mul(-distance * 0.01);
                        transparency.calc(Math.Exp);
                        transparency.mul(nearest.material.refraction);
                        color0.mac(transparency, tmpColor);
                    } else if (orientation == Orientation.Interior) {
                        color0.add(tmpColor);
                    }

                }
            }

            color = color0;
            return nearest;
        }
        public void render() {

            System.Threading.Parallel.For(0, screen.Size, i => {
            //for (int i = 0; i < screen.Size; i++) {
                Primitive prim;
                Vector3c color = new Vector3c();
                Vector3c node = new Vector3c();

                //System.Diagnostics.Debug.WriteLine("tid=" + System.Threading.Thread.CurrentThread.ManagedThreadId);

                Ray ray = screen.getRay(i);
                prim = raytrace(ray, ref color, ref node, 3, 1);
                screen.setPixel(i, color);
            });
            //}
        }
    }
}
